---
title: "Apps SDK Widgets"
description: "Build ChatGPT compatible widgets using react with zero boilerplate"
icon: "openai"
---

UI widgets allow you to create rich, interactive user interfaces that MCP clients can display. 
**mcp-use** supports multiple widget formats including **OpenAI Apps SDK** (this page) and [**MCP-UI**](/typescript/server/mcp-ui-resources).


<video alt="Apps SDK UI Widget Example" style={{maxWidth: '600px', borderRadius: '8px', marginBottom: '1.5rem', boxShadow: '0 2px 12px rgba(0,0,0,0.08)'}} muted autoPlay loop>
  <source src="/images/widget.mp4" type="video/mp4" />
</video>

## Quick Start

Start with the Apps SDK template which includes automatic widget registration:

```bash
npx create-mcp-use-app my-mcp-server --template apps-sdk
cd my-mcp-server
npm run dev
```

This creates a project with:
```
my-mcp-server/
├── resources/                      # 👈 Put your UI widgets here
│   ├── product-search-result/      # Folder-based widget example
│   │   ├── widget.tsx              # Widget entry point
│   │   ├── components/             # Reusable components
│   │   ├── hooks/                  # Custom hooks
│   │   ├── constants.ts
│   │   └── types.ts
│   └── styles.css                  # Global widget styles
├── public/                         # Static assets
│   └── fruits/                     # Example images
├── index.ts                        # Server entry point
├── package.json
├── tsconfig.json
└── README.md
```

### Folder Structure

Widgets can be organized in two ways: **single-file widgets** or **folder-based widgets**. Choose the organization style that best fits your widget's complexity.


For simple widgets, a single file is sufficient:

```
resources/
├── user-card.tsx          # Widget file
├── weather-display.tsx    # Another widget
└── product-card.tsx       # Yet another widget
```

Each `.tsx` file in the `resources/` folder becomes a widget. The widget name is derived from the filename (without extension).

For complex widgets with multiple components, hooks, or utilities, organize them in folders:

```
resources/
├── widget-name.tsx                    # Single-file widget
└── product-search-result/             # Folder-based widget
    ├── widget.tsx                     # Entry point (required)
    ├── components/                    # Sub-components
    │   ├── Accordion.tsx
    │   ├── AccordionItem.tsx
    │   ├── Carousel.tsx
    │   └── CarouselItem.tsx
    ├── hooks/                         # Custom hooks
    │   └── useCarouselAnimation.ts
    ├── constants.ts                   # Constants
    └── types.ts                       # Type definitions
```

**Key Points:**
- The folder name becomes the widget name (e.g., `product-search-result`)
- The entry point must be named `widget.tsx` (not `index.tsx`)
- You can organize sub-components, hooks, utilities, and types within the folder
- The `widget.tsx` file should export `widgetMetadata` and the default component

**Example Widget:**

```tsx
// resources/product-search-result/widget.tsx
import { McpUseProvider, useWidget, type WidgetMetadata } from 'mcp-use/react';
import { Accordion } from './components/Accordion';
import { Carousel } from './components/Carousel';
import type { ProductSearchResultProps } from './types';
import { propSchema } from './types';

export const widgetMetadata: WidgetMetadata = {
  description: 'Display product search results with filtering',
  inputs: propSchema,
};

const ProductSearchResult: React.FC = () => {
  const { props } = useWidget<ProductSearchResultProps>();
  
  return (
    <McpUseProvider autoSize>
      <div>
        <Carousel />
        <Accordion items={props.items} />
      </div>
    </McpUseProvider>
  );
};

export default ProductSearchResult;
```



## Widget Metadata 

Contains the information that the MCP resource (and the tool that exposes it) will use when are automatically built by mcp-use.
```typescript
import type { WidgetMetadata } from 'mcp-use/react';

export const widgetMetadata: WidgetMetadata = {
  // Required: Human-readable description
  description: string,
  
  // Required: Zod schema defining component props
  inputs: z.ZodObject<...>,
  
  // Optional: Apps SDK metadata (CSP, widget description, etc.)
  appsSdkMetadata?: {
    'openai/widgetDescription'?: string,
    'openai/widgetCSP'?: {
      connect_domains?: string[],
      resource_domains?: string[],
    },
    'openai/toolInvocation/invoking'?: string,
    'openai/toolInvocation/invoked'?: string,
    'openai/widgetAccessible'?: boolean,
    'openai/resultCanProduceWidget'?: boolean,
  },
}
```


**Note**: The CSP domains you specify will be merged with default trusted domains (like `*.oaistatic.com`, `*.oaiusercontent.com`, `*.openai.com`, and your server's base URL). This ensures your widget can access both your custom resources and OpenAI's required domains.

## Components & Hooks

mcp-use provides a comprehensive set of React components and hooks for building OpenAI Apps SDK widgets. These components handle common setup tasks like theme management, error handling, routing, and debugging.

### Components

| Component | Description | Link |
|-----------|-------------|------|
| **McpUseProvider** | Unified provider that combines all common React setup (StrictMode, ThemeProvider, BrowserRouter, WidgetControls, ErrorBoundary) | [McpUseProvider →](./widget-components/mcpuseprovider) |
| **WidgetControls** | Debug button and view controls (fullscreen/pip) with customizable positioning | [WidgetControls →](./widget-components/widgetcontrols) |
| **ErrorBoundary** | Error boundary component for graceful error handling in widgets | [ErrorBoundary →](./widget-components/errorboundary) |
| **Image** | Image component that handles both data URLs and public file paths | [Image →](./widget-components/image) |
| **ThemeProvider** | Theme provider for consistent theme management across widgets | [ThemeProvider →](./widget-components/themeprovider) |

### Hooks

| Hook | Description | Link |
|------|-------------|------|
| **useWidget** | Main hook providing type-safe access to all widget capabilities (props, state, theme, actions) | [useWidget →](./widget-components/usewidget) |



## Static Assets

Widgets can use static assets from a `public/` folder. The framework automatically serves these assets and copies them during build.

**Folder Structure**

```
my-mcp-server/
├── resources/
│   └── product-widget.tsx
├── public/                    # Static assets
│   ├── fruits/
│   │   ├── apple.png
│   │   ├── banana.png
│   │   └── orange.png
│   └── logo.svg
└── index.ts
```

**Using Public Assets**

In development, assets are served from `/mcp-use/public/`. In production, they're copied to `dist/public/` during build.

**Using the Image Component:**

```tsx
import { Image } from 'mcp-use/react';

function ProductWidget() {
  return (
    <div>
      {/* Paths are relative to public/ folder */}
      <Image src="/fruits/apple.png" alt="Apple" />
      <Image src="/logo.svg" alt="Logo" />
    </div>
  );
}
```

**Direct URL Access:**

The framework provides utilities for accessing public files:

- `window.__mcpPublicUrl`: Base URL for public assets (e.g., `http://localhost:3000/mcp-use/public`)
- `window.__getFile`: Helper function to get file URLs

```tsx
// Get public URL
const publicUrl = window.__mcpPublicUrl; // "http://localhost:3000/mcp-use/public"
const imageUrl = `${publicUrl}/fruits/apple.png`;

// Or use the helper
const imageUrl = window.__getFile?.('fruits/apple.png');
```


## Patterns

### Accessing Tool Input

Widgets are called by the model in the same way as tools. The `useWidget` hook provides access to the tool input, typed based on widget metadata schmea you defined.

```typescript
const { props } = useWidget();
```

**Example:**
```typescript
export const widgetMetadata: WidgetMetadata = {
  description: 'Display product search results with filtering',
  inputs: z.object({
    query: z.string(),
  }),
};

const ProductSearchResult: React.FC = () => {
  const { props } = useWidget();
  return <div>Query: {props.query}</div>;
};
```

### Widget State

Widgets can maintain state across interactions. State is persisted by the host, for example in ChatGPT:

```typescript
const { state, setState } = useWidget();

// Save state (persists to localStorage)
await setState({ favoriteCity: 'Tokyo', filters: { price: '$$' } });

// State persists across widget re-renders and page reloads
console.log(state?.favoriteCity); // 'Tokyo'

// Update state with function (like React useState)
await setState(prev => ({
  ...prev,
  favoriteCity: 'Paris'
}));
```

### Calling Other Tools

Widgets can call other MCP tools directly using `callTool`:

```typescript
const { callTool } = useWidget();

const handleSearch = async () => {
  try {
    const result = await callTool('search_cities', { 
      query: 'tokyo' 
    });
    // result.content contains the tool response
    console.log(result.content);
  } catch (error) {
    console.error('Tool call failed:', error);
  }
};
```

**Return Format:**
```typescript
const result: CallToolResponse = {
  content: [
    { type: 'text', text: '...' },
    // ... other content items
  ],
  isError?: boolean
};
```

### Display Mode Control

Request different display modes (inline, pip, or fullscreen):

```typescript
const { requestDisplayMode, displayMode } = useWidget();

const handleExpand = async () => {
  const result = await requestDisplayMode('fullscreen');
  // result.mode = 'fullscreen' (may be different if request denied)
  console.log('Display mode:', result.mode);
};

// Current display mode
console.log(displayMode); // 'inline' | 'pip' | 'fullscreen'
```

**Display Modes:**
- `'inline'` - Default embedded view in conversation
- `'pip'` - Picture-in-Picture floating window
- `'fullscreen'` - Full browser window (on mobile, PiP coerces to fullscreen)


You can user the `<WidgetControls />` to automatically add controls to your widget.
```tsx
import { WidgetControls } from 'mcp-use/react';

function MyWidget() {
  return (
    <McpUseProvider viewControls>
      <div>My widget content</div>
    </McpUseProvider>
  );
}
```
or 
```tsx
import { WidgetControls } from 'mcp-use/react';

function MyWidget() {
  return (
    <WidgetControls>
      <div>My widget content</div>
    </WidgetControls>
  );
}
```


## Custom Tools with Widgets

You can create custom tools that return widgets instead of relying solely on automatic widget registration. This is useful when you need to:
- Fetch data before displaying the widget
- Use different tool parameters than widget props
- Have multiple tools use the same widget
- Add custom logic or validation

### Using the widget() Helper

The `widget()` helper returns runtime data for a widget. Combine it with the `widget` config on the tool definition to set up all registration-time metadata:

```typescript
import { widget } from 'mcp-use/server';
import { z } from 'zod';

server.tool({
  name: 'get-weather',
  description: 'Get current weather for a city',
  schema: z.object({
    city: z.string().describe('City name')
  }),
  // Widget config sets all registration-time metadata
  widget: {
    name: 'weather-display',      // Must match a widget in resources/
    invoking: 'Fetching weather data...',
    invoked: 'Weather data loaded'
  }
}, async ({ city }) => {
  // Fetch weather data from API
  const weatherData = await fetchWeatherAPI(city);
  
  // Return widget with runtime data only
  return widget({
    data: {
      city,
      temperature: weatherData.temp,
      conditions: weatherData.conditions,
      humidity: weatherData.humidity,
      windSpeed: weatherData.windSpeed
    },
    message: `Current weather in ${city}`
  });
});
```

**Key Points:**
- **`widget: { name, invoking, invoked, ... }`** - Configures all widget metadata at registration time
- **`widget()` helper** - Returns only runtime data (structured content and optional message)
- **Widget must exist** - The widget name must match a `.tsx` file or folder in `resources/`
- **Disable auto-registration** - Set `exposeAsTool: false` in the widget's metadata if you only want it accessible through custom tools:

```tsx
// resources/weather-display/widget.tsx
export const widgetMetadata: WidgetMetadata = {
  description: 'Display weather information',
  inputs: propSchema,
  exposeAsTool: false  // Only accessible via custom tools
};
```

See [Tools Guide](./tools#returning-widgets-from-tools) for more information about the `returnsWidget` option.

## Configuration

### Base URL for Production

Set the `MCP_URL` environment variable or pass `baseUrl`:

```typescript
const server = new MCPServer({
  name: 'my-server',
  version: '1.0.0',
  baseUrl: process.env.MCP_URL || 'https://myserver.com'
});
```

This ensures:
- Widget URLs use the correct domain
- Apps SDK CSP automatically includes your server

Preferably set the variable at build time to have statically generated widget assets paths. 
If you don't set it at build time you must set it at runtime either by passing the `baseUrl` option to the `MCPServer` constructor or by setting the `MCP_URL` environment variable.

### Environment Variables

```env
# Server Configuration
MCP_URL=https://myserver.com

# For Static Deployments (e.g., Supabase)
MCP_SERVER_URL=https://myserver.com/functions/v1/my-function
CSP_URLS=https://myserver.com,...other domains
```

**Environment Variable Details:**

- `MCP_URL`: Base URL for widget assets and public files. Used by Vite's `base` option during build. Also used by the server to configure CSP.
- `MCP_SERVER_URL`: (Optional) MCP server URL for API calls. When set, URLs are injected at build time for static deployments where widgets are served from storage rather than the MCP server.
- `CSP_URLS`: (Optional) Additional domains to whitelist in widget Content Security Policy. Supports comma-separated list. For Supabase, use the base project URL without path (e.g., `https://nnpumlykjksvxivhywwo.supabase.co`). Required for static deployments where widget assets are served from different domains.

<Note>
**Static Deployments**: Set `MCP_URL` (for assets), `MCP_SERVER_URL` (for API calls), and `CSP_URLS` (for CSP whitelisting) when deploying to platforms like Supabase where widgets are served from static storage.

**Alternative CSP Configuration**: Instead of using the global `CSP_URLS` environment variable, you can configure CSP per-widget in your widget's `appsSdkMetadata['openai/widgetCSP']` (see [Apps SDK Metadata](#apps-sdk-metadata) section above).
</Note>


## Testing

### Using the Inspector

The MCP Inspector provides full support for testing widgets during development:

1. **Start your server**: `npm run dev`
2. **Open Inspector**: `http://localhost:3000/inspector`
3. **Test widgets**: Execute tools to see widgets render
4. **Debug interactions**: Use console logs and inspector features
5. **Test API methods**: Verify `callTool`, `setState`, etc. work correctly

See [Debugging ChatGPT Apps](/inspector/debugging-chatgpt-apps) for complete testing guide.



## Testing in ChatGPT

You need to enable the Developer Mode in ChatGPT to test widgets.

- **Enable developer mode**: Go to Settings → Connectors → Advanced → Developer mode.

- **Import MCPs**: In the Connectors tab, add your remote MCP server. It will appear in the composer's "Developer Mode" tool later during conversations.

- **Use connectors in conversations**: Choose Developer mode from the Plus menu and select connectors. You may need to explore different prompting techniques to call the correct tools. For example:

  - Be explicit: "Use the "Acme CRM" connector's "update_record" tool to …". When needed, include the server label and tool name.
  - Disallow alternatives to avoid ambiguity: "Do not use built-in browsing or other tools; only use the Acme CRM connector."
  - Disambiguate similar tools: "Prefer Calendar.create_event for meetings; do not use Reminders.create_task for scheduling."
  - Specify input shape and sequencing: "First call Repo.read_file with \{ path: "…" }. Then call Repo.write_file with the modified content. Do not call other tools."
  - If multiple connectors overlap, state preferences up front (e.g., "Use CompanyDB for authoritative data; use other sources only if CompanyDB returns no results").
  - Developer mode does not require search/fetch tools. Any tools your connector exposes (including write actions) are available, subject to confirmation settings.
  - See more guidance in Using tools and Prompting.
  - Improve tool selection with better tool descriptions: In your MCP server, write action-oriented tool names and descriptions that include "Use this when…" guidance, note disallowed/edge cases, and add parameter descriptions (and enums) to help the model choose the right tool among similar ones and avoid built-in tools when inappropriate.

## Next Steps

- [Creating Apps SDK Server](./creating-apps-sdk-server) - Complete guide
- [Debugging ChatGPT Apps](/inspector/debugging-chatgpt-apps) - Test widgets with Inspector
- [Project Templates](./templates) - Explore available templates

